package {{packageName}}.util.aspect;

import cn.hutool.core.util.ReflectUtil;
import {{packageName}}.util.annotation.DEAction;
import {{packageName}}.util.domain.DELogic;
import cn.ibizlab.util.domain.IEntity;
import cn.ibizlab.util.errors.BadRequestAlertException;
import cn.ibizlab.util.filter.ISearchContext;
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.Message;
import org.kie.api.builder.Results;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.ObjectUtils;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.jbpm.workflow.instance.node.EndNodeInstance;
import org.kie.api.event.process.DefaultProcessEventListener;
import org.kie.api.event.process.ProcessNodeTriggeredEvent;

/**
 * 实体处理逻辑切面（前后附加逻辑、实体行为调用处理逻辑）
 */
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(name = "ibiz.logic.type", havingValue = "Drools", matchIfMissing = true)
public class DELogicAspect {

    private static BpmnXMLConverter bpmnXMLConverter = new BpmnXMLConverter();
    private ConcurrentMap<String, DELogic> deLogicMap = new ConcurrentHashMap<>();
    private final ExpressionParser parser = new SpelExpressionParser();
    private static final String PARAM_LOGIC_CONTEXT = "srfdelogiccontext";
    private static final org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(DELogicAspect.class);

    @Around("execution(* {{packageName}}.core.*.service.*.*(..)) && !execution(* {{packageName}}.core.es.service.*.*(..))")
    public Object execute(ProceedingJoinPoint point) throws Throwable {
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();
        Object[] args = point.getArgs();
        DEAction action = getDEAction(point);
        //行为是否存在附加逻辑
        if (action != null && !ObjectUtils.isEmpty(action.file())) {
            IEntity entity = getEntity(target, methodName, args);
            Resource bpmnFile = new ClassPathResource(action.file());
            //执行处理逻辑
            if (bpmnFile != null && bpmnFile.exists()) {
                //支持节点配置多交互链接，交互链接中设置通过条件
                System.setProperty("jbpm.enable.multi.con","true");
                String bpmnId = DigestUtils.md5DigestAsHex(bpmnFile.getURL().getPath().getBytes());
                //读取逻辑bpmn文件
                DELogic logic = getDELogic(bpmnFile);
                if (logic != null) {
                    if (deLogicMap.containsKey(bpmnId) && logic.getMd5().equals(deLogicMap.get(bpmnId).getMd5())) {
                        logic = deLogicMap.get(bpmnId);
                    } else {
                        //编译bpmn drl
                        reloadLogic(logic);
                        deLogicMap.put(bpmnId, logic);
                    }

                    //执行逻辑
                    KieContainer container = logic.getContainer();
                    KieSession kieSession = container.getKieBase().newKieSession();
                    List<DELogic.Param> params = getLogicParams(logic);
                    //准备逻辑参数
                    EvaluationContext context = new StandardEvaluationContext();
                    for(DELogic.Param param : params){
                        String express = param.getExpress();
                        //当前实体default节点,使用entity作为参数
                        Object value = param.isEntity() ? entity: null;
                        //含表达式，执行spel表达式获取对象值
                        if(!ObjectUtils.isEmpty(express)){
                            value = parser.parseExpression(express).getValue(context);
                        }
                        //将逻辑参数设置到全局变量中
                        kieSession.setGlobal(param.getName(), value);
                    }
                    //构造逻辑上下文，将切点传入drools中，由drools执行
                    DELogic.Context logicContext = new DELogic.Context().setPoint(new DELogic.JoinPoint((MethodInvocationProceedingJoinPoint) point)).setLog(logger).setLogic(logic);
                    kieSession.setGlobal(PARAM_LOGIC_CONTEXT, logicContext);
                    //将逻辑设置到fact中,使节点能够被多次调用
                    kieSession.insert(logic);
                    //添加处理逻辑监听器
                    kieSession.addEventListener(new ProcessEventListener());
                    //启动jbpm，执行逻辑
                    kieSession.startProcess(logic.getProcess().getId());
                    //获取执行结果(drools执行切点返回值)
                    Object result = logicContext.getLogicReturnValue() != null ? logicContext.getLogicReturnValue() : logicContext.getPointValue();
                    return result;
                }
            }
        }
        return point.proceed();
    }

    /**
     * 获取实体行为
     * @param point
     * @return
     * @throws NoSuchMethodException
     */
    private DEAction getDEAction(ProceedingJoinPoint point) throws NoSuchMethodException {
        Object target = point.getTarget();
        Signature signature = point.getSignature();
        String methodName = signature.getName();
        DEAction action = null;
        if(signature instanceof MethodSignature){
            Method method = target.getClass().getMethod(methodName,((MethodSignature)point.getSignature()).getParameterTypes());
            if(method != null){
                action = AnnotatedElementUtils.findMergedAnnotation(method, DEAction.class);
            }
        }
        return action;
    }

    /**
     * 获取bpmn流程参数
     * @param logic
     * @return
     */
    private List<DELogic.Param> getLogicParams(DELogic logic) {

        List<DELogic.Param> params = new ArrayList<>();
        Process process = logic.getProcess();
        //获取主流程参数
        if(process != null){
            Map<String, List<ExtensionElement>> extensionElements = process.getExtensionElements();
            for (Map.Entry<String, List<ExtensionElement>> entry : extensionElements.entrySet()) {
                if ("metaData".equalsIgnoreCase(entry.getKey())) {
                    List<ExtensionElement> metaParams = entry.getValue();
                    for (ExtensionElement metaParam : metaParams) {
                        Map<String, List<ExtensionAttribute>> attribute = metaParam.getAttributes();
                        if (attribute.containsKey("name") && attribute.containsKey("type") && attribute.containsKey("express")) {
                            params.add(
                                    new DELogic.Param()
                                            .setName(attribute.get("name").get(0).getValue())
                                            .setType(attribute.get("type").get(0).getValue())
                                            .setExpress(attribute.get("express").get(0).getValue())
                            );
                        }
                    }
                }
            }
        }
        //获取子流程参数
        if (!ObjectUtils.isEmpty(logic.getRefLogic())) {
            for (DELogic subLogic : logic.getRefLogic()) {
                List<DELogic.Param> subLogicParams = getLogicParams(subLogic);
                if(subLogicParams != null)
                    params.addAll(subLogicParams);
            }
        }
        return params;
    }

    /**
     * 编译规则
     *
     * @param logic
     */
    private void reloadLogic(DELogic logic) throws IOException {
        KieServices kieServices = KieServices.get();
        KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
        for (Resource bpmn : logic.getRefRuleFiles()) {
            kieFileSystem.write(ResourceFactory.newUrlResource(bpmn.getURL()));
        }
        KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem).buildAll();
        Results results = kieBuilder.getResults();
        if (results.hasMessages(Message.Level.ERROR)) {
            throw new BadRequestAlertException(String.format("编译实体处理逻辑 [%1$s] 发生异常, %2$s", logic.getName(), results.getMessages()), "DELogicAspect", "reloadLogic");
        }
        KieContainer kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
        logic.setContainer(kieContainer);
    }

    /**
     * 获取逻辑配置，单次仅加载附加行为为处理逻辑、行为类型为处理逻辑的两类bpmn
     * <p>
     * 1.附加内部逻辑： bpmn 中通过 callActivity 直接调用
     * 2.行为类型为处理逻辑：  bpmn 中通过 callActivity 直接调用
     *
     * @param bpmnFile
     * @return
     */
     private DELogic getDELogic(Resource bpmnFile) {
        DELogic logic = null;
        XMLStreamReader reader = null;
        InputStream bpmn = null;
        try {
            if (bpmnFile.exists()) {
                XMLInputFactory factory = XMLInputFactory.newInstance();
                bpmn = bpmnFile.getInputStream();
                reader = factory.createXMLStreamReader(bpmn);
                BpmnModel model = bpmnXMLConverter.convertToBpmnModel(reader);
                Process mainProcess = model.getMainProcess();
                if (mainProcess == null) {
                    return null;
                }
                log.debug("正在加载 BPMN:{}", bpmnFile.getURL().getPath());
                Map<String,DELogic> refLogicsMap = new LinkedHashMap<>();
                Map<String,Resource> refFilesMap = new LinkedHashMap<>();
                //自己 bpmn 及 drl
                refFilesMap.put(bpmnFile.getURL().getPath(),bpmnFile);
                Resource drlFile = getDrl(bpmnFile);
                if (drlFile != null && drlFile.exists()) {
                    refFilesMap.put(drlFile.getURL().getPath(),drlFile);
                    log.debug("正在加载 DRL:{}", drlFile.getURL().getPath());
                }
                //子 bpmn 及 drl
                if (!ObjectUtils.isEmpty(model.getMainProcess()) && !ObjectUtils.isEmpty(model.getMainProcess().getFlowElementMap())) {
                    for(FlowElement item : model.getMainProcess().getFlowElementMap().values()){
                        if (item instanceof CallActivity) {
                            CallActivity subBpmn = (CallActivity) item;
                            //获取关联行为的bpmn文件路径
                            ClassPathResource subBpmnFile = new ClassPathResource(subBpmn.getName());
                            if (subBpmnFile != null && !refFilesMap.containsKey(subBpmnFile.getURL().getPath())) {
                                DELogic refLogic = getDELogic(subBpmnFile);
                                if (refLogic != null) {
                                    refLogicsMap.put(subBpmnFile.getURL().getPath(), refLogic);
                                    if (!ObjectUtils.isEmpty(refLogic.getRefRuleFiles())) {
                                        for(Resource e : refLogic.getRefRuleFiles()){
                                            refFilesMap.put(e.getURL().getPath(), e);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                logic = new DELogic();
                logic.setId(mainProcess.getId());
                logic.setName(mainProcess.getName());
                logic.setProcess(mainProcess);
                logic.setRefLogicMap(refLogicsMap);
                logic.setRefRuleFilesMap(refFilesMap);
                logic.setMd5(getMd5(refFilesMap.values()));
            }
        } catch (Exception e) {
            log.error("执行处理逻辑失败" + e);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (bpmn != null) {
                    bpmn.close();
                }
            } catch (Exception e) {
                log.error("执行处理逻辑失败" + e);
            }
        }
        return logic;
    }

    @SneakyThrows
    private Resource getDrl(Resource bpmn) {
        String filePath = bpmn instanceof FileSystemResource ? bpmn.getURL().getPath() : ((ClassPathResource) bpmn).getPath();
        filePath = filePath.replace(".bpmn", ".drl");
        return bpmn instanceof FileSystemResource ? new FileSystemResource(filePath) : new ClassPathResource(filePath);
    }


    /**
     * 获取实体
     * @param service
     * @param method
     * @param args
     * @return
     */
    protected IEntity getEntity(Object service, String method, Object[] args) {
        IEntity entity = getEntity(args);
        //尝试通过service.getEntity()查找entity
        if (entity == null) {
            Method entityMethod = ReflectUtil.getMethodByName(service.getClass(), "getEntity");
            if (entityMethod != null) {
                Object result = ReflectUtil.invoke(service, entityMethod, null);
                if (result != null && result instanceof IEntity)
                    return (IEntity) result;
            }
        }
        if (entity == null)
            throw new RuntimeException(String.format("未找到服务对象[%1$s]对应的实体信息", service.getClass().getSimpleName()));

        return entity;
    }

    /**
     * 从参数列表中获取实体
     * @param args
     * @return
     */
    protected IEntity getEntity(Object[] args) {
        if (ObjectUtils.isEmpty(args))
            return null;

        for (Object arg : args) {
            if (arg instanceof IEntity) {
                return (IEntity) arg;
            } else if (arg instanceof List) {
                for (Object obj : (List) arg) {
                    IEntity entity = getEntity(new Object[]{obj});
                    if (entity != null)
                        return entity;
                }
            }else if(arg instanceof ISearchContext){
                return null;
            }
        }
        return null;
    }

    /**
     * 判断当前行为是否为内置行为
     *
     * @param service
     * @param action
     * @param args
     * @return
     */
    protected boolean enableMethod(Object service, String action, Object[] args) {
        if ("createBatch".equalsIgnoreCase(action)
                || "updateBatch".equalsIgnoreCase(action)
                || "saveBatch".equalsIgnoreCase(action)
                || "removeBatch".equalsIgnoreCase(action)
                || (action.startsWith("removeBy") && !ObjectUtils.isEmpty(args) )
                || (action.startsWith("saveBy") && !ObjectUtils.isEmpty(args) && args.length == 2)
                || (getEntity(args) == null)
        )
            return false;
        else
            return true;
    }

    /**
     * 获取bpmn md5
     *
     * @param subFiles
     * @return
     */
    private String getMd5(Collection<Resource> subFiles) {
        try {
            StringBuffer buffer = new StringBuffer();
            for (Resource file : subFiles) {
                InputStream bpmnFile = null;
                try {
                    bpmnFile = file.getInputStream();
                    if (!ObjectUtils.isEmpty(bpmnFile)) {
                        String strBpmn = IOUtils.toString(bpmnFile, "UTF-8");
                        buffer.append(strBpmn);
                    }
                } catch (Exception e) {
                    log.error("处理逻辑版本检查失败" + e);
                } finally {
                    if (bpmnFile != null) {
                        bpmnFile.close();
                    }
                }
            }
            if (!ObjectUtils.isEmpty(buffer.toString())) {
                return DigestUtils.md5DigestAsHex(buffer.toString().getBytes());
            } else {
                return null;
            }
        } catch (Exception e) {
            log.error("处理逻辑版本检查失败" + e);
            return null;
        }
    }

    public static class ProcessEventListener extends DefaultProcessEventListener {

        private static final String LOGIC_NODETYPE_LOGICPARAM = "logicParam";

        /**
         * 获取bpmn结束节点返回值，并设置到上下文中
         * @param event
         */
        public void beforeNodeTriggered(ProcessNodeTriggeredEvent event) {
            if (event.getNodeInstance() != null && event.getNodeInstance() instanceof EndNodeInstance) {
                EndNodeInstance endNode = ((EndNodeInstance) event.getNodeInstance());
                String processId = endNode.getProcessInstance().getProcessId();
                String flowElementId = endNode.getMetaData() != null && endNode.getMetaData().containsKey("UniqueId") ? endNode.getMetaData().get("UniqueId").toString() : null;
                Object context = event.getKieRuntime().getGlobal(PARAM_LOGIC_CONTEXT);
                if (context != null && context instanceof DELogic.Context && !ObjectUtils.isEmpty(processId) && !ObjectUtils.isEmpty(flowElementId)) {
                    DELogic.Context logicContext = (DELogic.Context) context;
                    Map<String, Process> processMap = logicContext.getLogic().getRefProcessMap();
                    FlowElement flowElement = processMap != null && processMap.containsKey(processId)? processMap.get(processId).getFlowElement(flowElementId) : null;
                    if (flowElement == null)
                        return;

                    Map<String, ExtensionAttribute> nodeParams = getProcessParams(flowElement, "metaData");
                    if (!ObjectUtils.isEmpty(nodeParams)) {
                        String type = nodeParams.get("type").getValue();
                        String name = nodeParams.get("name").getValue();
                        Object value = null;
                        if (LOGIC_NODETYPE_LOGICPARAM.equalsIgnoreCase(type) && !ObjectUtils.isEmpty(name)) {
                            value = event.getKieRuntime().getGlobal(name);
                        }
                        ((DELogic.Context) context).setLogicReturnValue(value);
                    }
                }
            }
        }

        /**
         * 获取bpmn节点参数
         * @param element
         * @param property
         * @return
         */
        protected Map<String, ExtensionAttribute> getProcessParams(FlowElement element, String property) {
            Map<String, ExtensionAttribute> params = new HashMap<>();
            if(element == null){
                return params;
            }
            List<ExtensionElement> formProps = element.getExtensionElements().get(property);
            if (formProps == null) {
                return null;
            }
            for (ExtensionElement prop : formProps) {
                if (prop.getAttributes() == null) {
                    return null;
                }
                Map<String, List<ExtensionAttribute>> attributes = prop.getAttributes();
                attributes.entrySet().forEach(entry->{
                    if (!ObjectUtils.isEmpty(entry.getValue())) {
                        params.put(entry.getKey(), entry.getValue().get(0));
                    }
                });
            }
            return params;
        }
    }
}